Skip to content

fix(metadata-protocol,runtime): a seed load that throws part-way still reports landed ids for the datasets that finished - #30519

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-crfqzt
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-crfqzt

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

Requested by Yibo Wang · project thread

Fixes #30429

Clause-②: yes (widening: a thrown load's receipt carries landed for the datasets that finished, where it carried none)
Metadata-protocol: semantic
Skills-docs: none owed — no skill, eval or doc teaches seedApplied, landedIds or SeedLoadPartialWrites (git grep -n "seedApplied\|landedIds\|SeedLoadPartialWrites" -- skills content/docs ':!content/docs/references' ':!content/docs/releases': 0 hits; control git grep -c "publish" -- skills/objectstack-ai/SKILL.md: 5); the generated content/docs/references/api/protocol.mdx is regenerated here. The skills lane is paused; nothing is owed to it from this PR.
Prior rulings read: seedapplied,seedloadpartialwrites,landed → 16 hits; ADR-0038 Decision §4, ADR-0056 D10, ADR-0058 D7, ADR-0060 D5, ADR-0061 D2, ADR-0076 D10, ADR-0090 D10, ADR-0093 D9, ADR-0094 D5; thread: none; repo: objectstack-ai/objectstack

维护者速读

  • 改了什么:种子加载中途抛错时,发布回执 seedApplied 现在也带 landed,列出抛错前已经跑完的数据集落下的行 id(同一个投影、同样分 inserted/matched 两张表、同样 200 上限);抛错的那个数据集不列出。运行时的兜底种子应用走同一读取与投影,失败回执也补上了 schema 要求的 inserted/updated 条数,并在 error 里点名停在哪个对象。
  • 为什么改:metadata-protocol: the publish receipt's seedApplied drops the landed row ids the seed loader computes, so cloud cannot tell which rows an AI build's seed landed on (objectstack-ai/cloud#3598 ruling A) #30348 之后,加载一旦中途失败,回执只有条数没有 id,cloud 的「清理示例数据」拿不到前面已落库数据集的行,这些示例行就清不掉。
  • 风险与回滚:只在失败回执上多给一个已有的可选字段;成功路径不变。landed 缺席的含义收窄为"没有运行加载",抛错前一个数据集都没跑完时给 []。回滚即还原本 PR。
  • 先看:packages/metadata-protocol/src/seed-loader.ts 的 SeedLoadPartialWrites.finished 与 notePartialWrites;packages/metadata-protocol/src/protocol-publish-batch.ts 的 catch;packages/runtime/src/domains/packages.ts 兜底 catch。
  • 两分钟验证:pnpm --filter @objectstack/objectql exec vitest run src/publish-seed-receipt-landed-ids.test.ts,预期 10 例全绿,其中"load that threw"一例的 landed 只含先跑完的对象。
  • 需要你做的:无,席位落地。

What changes

  • @objectstack/metadata-protocol (seed-loader.ts): SeedLoadPartialWrites.finished. notePartialWrites snapshots the finished datasets' { object, landedIds, insertedIds }, in load order, off the results it already keeps (loadProgress.finished). Nothing new is tracked; the ids are the ones each finished dataset's returned result carries. Empty when the throw came before any dataset finished.
  • @objectstack/metadata-protocol (protocol-publish-batch.ts): applySeedBodies' catch adds landed: seedAppliedLanded(written.finished) whenever a partial-writes reading exists. Same projection as the returned path (seedAppliedLanded), so the same two lists, the same per-object fold and the same 200 cap. No reading (the request parse refused, or the loader module never loaded) leaves landed absent.
  • @objectstack/metadata-protocol (index.ts): seedLoadPartialWrites and SeedLoadPartialWrites are exported from the package root, so the runtime fallback reads the same record.
  • @objectstack/runtime (domains/packages.ts): the route-level fallback seed apply's catch reads seedLoadPartialWrites(e) and reports the same landed. It had the same thrown path (applyPublishedSeeds → loader.load throws → catch with success: false, error). It now also reports inserted / updated from the same reading (the response schema requires both; this catch used to send neither) and names the object whose seed stopped the load in error (seed apply stopped at the seed for object 'X': …), exactly as applySeedBodies' catch does; the caught sentence itself is still withheld unless declared. No reading: zero counts, the bare sentence, no landed.
  • @objectstack/spec (api/protocol.zod.ts): the shared seedApplied.landed description (both publish responses) and the PublishMetaItemResponseSchema docblock now say what landed covers on a thrown load (the datasets that finished, empty when none had; not the one that threw) and when it is still absent (no load ran, or a producer that reports no ids). Schema shape unchanged. content/docs/references/api/protocol.mdx regenerated.
  • protocol-publish-batch.ts: the older comment above the catch's counters now agrees with the new one beside landed (no reading means no load ran; a reading with zero counts is a load that threw before writing a row).
  • .changeset/30429-seed-receipt-landed-on-throw.md: spec minor, metadata-protocol minor, runtime patch, with a "Supersedes, in the same release" paragraph for the sentence in the unreleased .changeset/30348-seed-receipt-landed-ids.md that this makes false.

The dataset that threw: omitted, and why

Measured at seed-loader.ts: the dataset that throws can have landed rows before the throw. Its batched inserts flush every insertBatchSize records (flushPendingInserts), so earlier batches are written, and a throw can come after a row was written (the seed-file settle, pinned in seed-loader-seed-files.test.ts: pw_doc's D1 is in the store when the storage service throws). Those rows are in inserted / updated and the object is named in error, but they are not listed in landed:

  • Its ids are only partly known when it throws. A batched insert whose response was lost may have committed rows whose ids never came back; the loader re-reads by key on its retry for exactly that reason. A list of the ids it did see would read as complete while missing rows, which is the one thing a reader of the ids cannot check.
  • A finished dataset's list is complete, so "the datasets that finished" is a line a reader can rely on. This is the card's default.

A reader that wants to know whether unlisted rows exist compares inserted with the listed inserted.totals; error names the object.

What landed answers now

Receipt landed
load returned every dataset (unchanged)
load threw after some datasets finished those datasets only
load threw in pass 2 (every dataset finished) every dataset, no failed object
load threw before any dataset finished [] (the loader ran; nothing finished)
no load ran (seed bodies refused at parse, no readable bodies) absent

Why

#30348 (PR #30427) put the ids on the receipt, but only on the returned-load path; its contract review named the gap at applySeedBodies' catch. After a partial publish cloud's "clear sample data" had no ids for the datasets that did land, so those sample rows could never be cleared by id.

Verification

Head 4c02bddf8, under os-verify-lock, NODE_OPTIONS=--max-old-space-size=4096:

  • pnpm --filter @objectstack/metadata-protocol test: Test Files 316 passed, 4 skipped (320), Tests 22425 passed, 26 skipped. typecheck exit 0.
  • pnpm --filter @objectstack/objectql test: Test Files 551 passed (551), Tests 10989 passed. typecheck exit 0.
  • pnpm --filter @objectstack/runtime test: Test Files 543 passed (543), Tests 8647 passed, 19 skipped. typecheck exit 0.
  • pnpm --filter @objectstack/spec test: Test Files 815 passed (815), Tests 25622 passed, 1 todo. typecheck exit 0.
  • Pins:
    • packages/objectql/src/publish-seed-receipt-landed-ids.test.ts (real protocol, real ObjectQL engine, real loader over an in-memory driver; ids read back from the engine). The old pin "a load that threw reports no ids" is replaced by: a good seed (with a customer's pre-existing row its key matches) lands, a later seed naming a missing object throws; the receipt lists the good seed's created id under inserted and the customer's row under matched, has no entry for the object that threw, and PublishPackageDraftsResponseSchema keeps it. Plus: a load that throws before any seed finished reports landed: [], not absent.
    • packages/runtime/src/domains/packages-seed-apply-landed-thrown.test.ts (new, the fallback door with the real SeedLoaderService): positive control on a returned load; a second dataset's existing-row read throws after the first dataset wrote, and the receipt lists the first, reports inserted: 1, updated: 0, and error names the stopped object with the driver sentence withheld; a failure before any load ran leaves landed absent.
    • packages/metadata-protocol/src/seed-loader-deferred-failure.test.ts (+1): a throw in pass 2, after both datasets of a circular pair wrote: the reading has no failedObject, finished lists both datasets with their store ids, and seedAppliedLanded(finished) names every row. Pass 2 guards its own driver traffic (a failed back-fill write is counted, a failed lookup probe is a miss; both measured), so the throw is staged at its one unguarded call, the logger.error its back-fill failure reports through.
    • packages/metadata-protocol/src/seed-loader-seed-files.test.ts: seedLoadPartialWrites now carries finished with the finished dataset's store ids, and leaves out the dataset that threw although its row D1 was written; a throw before any dataset reads finished: [].
Reverse verification (two mutations, each restored by scripts/ablation-replace.mjs to the HEAD blob)

The objectql and runtime pins resolve @objectstack/metadata-protocol from dist, so each mutation was rebuilt and ablation-dist-preflight.mjs confirmed the marker in 2 built files before reading a result. Directions predicted before running.

  1. The loader carries no finished ids (finished: finished.map(...) → finished: (globalThis as any).ABL30429A ?? [], blob a7986c958ece → c3a35b1b3aa0): objectql landed pins 1 failed | 9 passed (the thrown-after-finish case; the before-any-finished [] case holds either way), runtime fallback 1 failed | 2 passed, seedLoadPartialWrites unit 1 failed | 2 passed. Restored: blob == HEAD.

  2. The catch drops landed (...(written ? { landed: ... } : {}) → ...((globalThis as any).ABL30429B ?? {}), blob eccb3c495621 → cb59f9935201): objectql landed pins 2 failed | 8 passed (both thrown cases). Restored: blob == HEAD.

  3. Round 1, the runtime catch's counts (inserted: written?.inserted ?? 0, → inserted: 0, in packages.ts, blob 39d41e8df5cb → 8a37f8d6e007; runtime resolves its own src): packages-seed-apply-landed-thrown.test.ts 1 failed | 2 passed (the thrown case). Restored: blob == HEAD, git diff HEAD empty.

Restore leg for 1 and 2: rebuilt, --absent preflight green for both markers, git diff HEAD empty, the three files back to 10/10, 3/3, 3/3. A first attempt planted the markers as comments, which the build strips; the preflight refused it (marker absent from dist) before any test ran, and it is discarded.

Gates
  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran gates-ran.list: "105 derived famil(ies) accounted for — 105 run, 0 NOT-MEASURED (a DERIVED zero — all 105 recorded an exit code and none of them is 3)", exit 0. Every line exit 0.
  • The serial pass ran at be4d35b5 (the implementation commit); 4c02bddf8 adds only the changeset's "Supersedes" paragraph, and the changeset gates (check-empty-changeset, check-changeset-no-major) plus check:generated and check:nul-bytes were re-run on 4c02bddf8, exit 0.
  • Three families exited 3 (prerequisite) on the first pass and were re-run green: check-plugin-teardown-shape --self-test (its pinned fixture commit fetched), check:skill-examples and check:dual-build-cjs-loads (after a full turbo run build, 119/119).
  • Prior rulings read is from node scripts/pm/check-prior-rulings.mjs --card 30429 --terms seedApplied,SeedLoadPartialWrites,landed. ADR-0038 Decision §4 (L3 probes attach to the publish response "like seedApplied") does not rule on the receipt's ids; the other hits match the word "landed" in unrelated decisions. The card's thread holds no ruling.
  • check-changeset-no-major with this body as the event: exit 0 on Metadata-protocol: semantic (spec and metadata-protocol minor). Controls: none exits 1 (the minor entries are unaccounted for); added also exits 0, so the gate checks the level, not the kind. The kind is semantic by reading: no schema key is added or removed (the schema shape is byte-identical; only the description moves), and an existing key's absence changes meaning on a thrown load, from "not reported" to "no load ran".
  • No ! in the title and no (narrowing) arm: no full dogfood run. CI owns the rest of the farm.

Acceptance notes

  • The seed-ownership claim (claim-seed-ownership.ts, seed-settlement.ts) still reads landedIds: out of scope per the card.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic (description text only).
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes (CI-owned run).
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the 维护者速读 block excepted, per project convention).
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGufzht5n1PKh96TqTfrvV

…l reports landed ids for the datasets that finished

SeedLoadPartialWrites carries the finished datasets' landedIds/insertedIds,
and both receipt producers (applySeedBodies' catch and the runtime fallback)
fold them through seedAppliedLanded. The dataset that threw stays out of
landed: its ids are only partly known when it throws.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzht5n1PKh96TqTfrvV
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/metadata-protocol, @objectstack/runtime, @objectstack/spec, touching 6 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/permissions/system-context.mdx (via handlePackagesRequest (symbol, a top-level function))
What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 103 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 175 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json fa9c0aa2a0a8c2752f83b57765f3aae978ae196a → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 03e546fa3e9379289110d2a05e09f2c62b3bebfa — the merge of head 7f2442b0338fb295c8ffd047fececd535ea75938 into base fa9c0aa2a0a8c2752f83b57765f3aae978ae196a, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 03e546fa3e9379289110d2a05e09f2c62b3bebfa && git checkout 03e546fa3e9379289110d2a05e09f2c62b3bebfa
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fa9c0aa2a0a8c2752f83b57765f3aae978ae196a 7f2442b0338fb295c8ffd047fececd535ea75938 && git checkout -B drift-repro fa9c0aa2a0a8c2752f83b57765f3aae978ae196a && git merge --no-ff 7f2442b0338fb295c8ffd047fececd535ea75938

node scripts/docs-audit/affected-docs.mjs --json fa9c0aa2a0a8c2752f83b57765f3aae978ae196a

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs fa9c0aa2a0a8c2752f83b57765f3aae978ae196a → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

Contract-review-claim: session_01WGufzfgvGexKn9vLHsy89K · head 4c02bdd · 2026-11-05T14:03Z

Full review of the first head at CONTRACT_REVIEW_TIER (a thrown seed load's receipt now lists the finished datasets in seedApplied.landed; an existing key's absence changes meaning, declared semantic).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4c02bddf8e26935368f9170ae5fb2487c9f2817b
Local-runs: probe — node scripts/pm/check-prior-rulings.mjs --card 30429 --terms seedApplied,SeedLoadPartialWrites,landed (report-only) to compare the body's line; no vitest, no build
Scope: the PR diff against merge-base f506b79d85 (all 10 files, two commits be4d35b5 and 4c02bddf); packages/metadata-protocol/src/seed-loader.ts at head (SeedLoadPartialWrites :578-600, seedLoadPartialWrites :609-611, loadProgress :764-767, load :809-819, notePartialWrites :827-841, loadDatasets :925-1090 with allResults :944-945 and the push :1062, loadDataset :1206-1223 and its return :2293-2327, the retry re-read :1375-1407, resolveDeferredUpdates :2603-2890); packages/metadata-protocol/src/seed-applied-landed.ts (SeedLandingResult :24-28, cappedList :30-36, seedAppliedLanded :49-66); packages/metadata-protocol/src/protocol-publish-batch.ts (applySeedBodies :82-191, the lazy import :91-92, the returned path :139, the catch :141-190); packages/metadata-protocol/src/index.ts :284-291; packages/runtime/src/domains/packages.ts (:100, the fallback catch :2105-2153, applyPublishedSeeds :3847-3990); packages/runtime/src/domains/packages-seed-apply-landed-thrown.test.ts (3 cases); packages/objectql/src/publish-seed-receipt-landed-ids.test.ts (harness :1-168, the two thrown pins :287-327, the old pin on main :287-300); packages/metadata-protocol/src/seed-loader-seed-files.test.ts :522-581; packages/spec/src/api/protocol.zod.ts (SEED_APPLIED_LANDED_DESCRIPTION :1493-1501, the docblock :1534-1539, the two landed keys :1601 and :1902); packages/spec/src/api/protocol.test.ts :3034-3074; content/docs/references/api/protocol.mdx :2549 and :2650; the generated-ledger census at origin/main (packages/spec/json-schema.manifest/api.json, packages/spec/api-surface/api.json, the three files that carry the landed description); .changeset/30348-seed-receipt-landed-ids.md and the new changeset; scripts/check-changeset-no-major.mjs :1-80 and :900-945; the seat order /mnt/project-files/seat3-orders/30429.md; card #30429 body and 4 comments (triage 6657518253, the two claims, os-dev-report 6659938998); PR #30427's contract review 6656296448; PR #30519 body, 2 comments, 0 reviews; check-runs on the head.

① Derived judgments

  1. The partial-writes path carries the finished datasets' ids, and both producers fold them through the one projection. loadProgress.finished IS allResults (:945), a dataset's result is pushed only after loadDataset returns (:1062, return at :2311), and the dataset loading when the throw hits sits in loading (:1223) and never in finished; so notePartialWrites (:836) snapshots exactly the datasets that finished, in load order, taking object, landedIds and insertedIds off each result. The id arrays are built once at :2293-2308 and nothing writes to them afterwards (pass 2 touches referencesResolved and the error lists on allResults, the feat(spec,metadata-protocol): the publish receipt names the rows a seed created and the rows it only matched #30427 review's reading, re-confirmed by the allResults census at :2662 and :2890), so copying the references is safe and the "snapshot of the ids alone" comment (:834-835) is right about what it protects against. applySeedBodies' catch adds ...(written ? { landed: seedAppliedLanded(written.finished) } : {}) (:177), the same function the returned path calls at :139, and seedAppliedLanded is the only place cappedList (:30-36) and the inserted-versus-matched split (:61-65) live; SeedLandingResult is imported as a type from the projection module (:51), so no second copy of the shape either. One projection, same two lists, same 200 cap. The loader's catch wraps every throw out of load (:816-818), including one before loadProgress is set (:936-942), so every error that came out of a load carries a reading.
  2. The failing dataset is omitted, and the reason holds. The loader's batched flush re-reads existing rows by key on attempt > 1 because a prior attempt may have committed before its response was lost (:1402-1406); a throw that lands inside or right after such a flush leaves internalIdByRecordIndex short of rows the table holds, so a list of the ids the loader did see for that dataset would read as complete while missing rows. A finished dataset has no such hole. The seed-files pin measures the concrete case: pw_doc's D1 is in the store when the storage service throws (:559-560) and finished names pw_tag only (:566-568). The reason is stated in the finished docblock (:592-597), the catch comment (:173-176), the spec docblock (:1536-1538) and the body; the describe (:1498-1499) states the rule (not listed; rows in the counts, object in error) without the reason, which is the right split for a wire description. Empty versus absent: finished is [] whenever no dataset finished (:828, :836), so any throw out of load reports landed: [], and landed is absent only when the error did not come out of a load: the request parse refusal (:127), the loader import (:91), and in the runtime required services unavailable (:3859), a seed-body read failure (:3886) and no readable seed bodies (:3950). The describe's "the apply failed before any load ran" is exactly that set. [] is consistent with the returned-load path: a request whose datasets all filter out returns buildEmptyResult (:981) with no results, so the returned receipt also says landed: [] for "the loader ran, no object ran"; "one entry per object the loader ran, empty lists included" covers both. The describe does not spell the [] case out; the changeset and the objectql pin at :315-327 do (should-fix 2).
  3. The runtime fallback gets the same fix the same way. applyPublishedSeeds has the same thrown path (loader.load :3981 with no catch of its own, caught at the door :2105); the door's catch now reads seedLoadPartialWrites(e) (:2134) and spreads landed: seedAppliedLanded(written.finished) (:2138) through the same two symbols imported from the package root (:100; index.ts :284-291 exports the reader and its type). What the fallback still does not carry is inserted / updated on a throw (:2135-2152), although PublishPackageDraftsResponseSchema.seedApplied declares both required and the new describe says the failing dataset's "rows are in the counts". The body names this as pre-existing and carried; it is, but written is now in hand at :2134 and the counts are two more spreads (should-fix 1).
  4. Spec and ripples. The shared describe (:1493-1501) and the PublishMetaItemResponseSchema docblock (:1534-1539) say what a thrown load reports (finished datasets only; the one that threw not listed, rows in the counts, object in error) and when landed is still absent (no load ran, or a producer that reports no ids); the schema shape is byte-identical (:1601, :1902 unchanged). Ripples: on origin/main exactly three files carry the description text (the metadata-protocol: the publish receipt's seedApplied drops the landed row ids the seed loader computes, so cloud cannot tell which rows an AI build's seed landed on (objectstack-ai/cloud#3598 ruling A) #30348 changeset, protocol.zod.ts, content/docs/references/api/protocol.mdx); the mdx's two cells (:2549, :2650) now carry the new describe verbatim, which is the generator's output, not a hand edit. json-schema.manifest/api.json is a name list (no description hashes), api-surface/api.json lists spec symbols (none added), and the llms route builds from the mdx at docs build time, so no other committed artifact owes a change. check:generated exit 0 in the dev's report; Lint · docs & code guards was in_progress at review time.
  5. Pins and ablations. objectql, real protocol over a real engine and the real loader: the old pin (main :287-300, 'landed' in res.seedApplied false) is replaced by :287-313, where a customer row shares R1's key so both lists are exercised, a_good lands three records (inserted: 2, created ids read back from the engine, matched: [customer.id]), z_missing throws, error names it, landed has one entry and no entry for the object that threw, and PublishPackageDraftsResponseSchema.parse keeps it on a failed apply (:311); :315-327 pins landed: [] when the first dataset throws. Runtime, the real HttpDispatcher and the real SeedLoaderService over a mocked engine: positive control, a second dataset's existing-row read throwing after the first wrote (find asserted called on BROKEN), and getMetaItem failing before any load with 'landed' in seedApplied false (:87-116). Loader unit: finished with the store's own ids and the written-but-unfinished pw_doc left out (:561-568), finished: [] before any dataset (:580). Ablation A (the snapshot neutralised in dist) reddens 1/10, 1/3, 1/3 and ablation B (the catch spread neutralised) reddens 2/10, each with blob hashes and a restore leg; the counts match the reading (A cannot redden the [] pin, B reddens both thrown pins). Both real. The spec pin at protocol.test.ts :3050-3056 asserts absence on a hand-built receipt that carries no landed, not on a thrown load, so it is correct as left. Gap: no pin for a throw in pass 2 (resolveDeferredUpdates :1082), where every dataset finished, failedObject is unset and landed lists them all; by reading that is what :828-836 and :167-169 give (should-fix 3).
  6. Declarations. Body and changeset both carry Clause-②: yes (widening: …) and Metadata-protocol: semantic; the changeset grades spec minor, metadata-protocol minor, runtime patch. Under the gate's rule (:909-910) a kind needs at least one moved package at minor or above, which two entries meet; Check Changeset is green on the head, and the dev's controls (none exits 1, added exits 0) show the gate checks the level, not the kind. semantic over added is right by reading: no authorable key, value or default is added (no authorable-surface/ or authorable-defaults/ shard moves, the schema shape is unchanged), and what moves is the meaning of an existing key's absence on a thrown load (from "not reported, including a load that threw" to "no load ran"), which is the kind the gate's own text calls "a changed meaning" (:931). metadata-protocol minor is earned by its own surface: two new root exports (seedLoadPartialWrites, SeedLoadPartialWrites) and a new required member finished on an exported record. The "Supersedes" paragraph names the sentence in the unreleased metadata-protocol: the publish receipt's seedApplied drops the landed row ids the seed loader computes, so cloud cannot tell which rows an AI build's seed landed on (objectstack-ai/cloud#3598 ruling A) #30348 changeset this makes false, so the release notes will not carry two contradictory readings of absent landed.
  7. Scope, body and lines. No cloud file; claim-seed-ownership.ts and seed-settlement.ts are not in the diff and still read landedIds. The body's four-row table of what landed answers matches :139, :177 and the loader's finished by reading; the "no reading" cases it names (parse refusal, loader import) are :127 and :91. Prior rulings read: is byte-identical to the script's output on the same terms. The 维护者速读 has its six lines and its two-minute check names the objectql file at 10/10, the count the ablation report also gives. 234 changed lines, no governed surface in the file list.

② Semver level

@objectstack/spec minor, @objectstack/metadata-protocol minor, @objectstack/runtime patch, with Metadata-protocol: semantic and Clause-②: yes (widening). The receipt widens (a failed apply now carries an optional key it did not), no shape changes, and an existing key's absence narrows in meaning; the level follows the kind, and the changeset's grades are the ones the gate owes.

③ Boundary flags

  • The line the change draws (finished datasets listed, the failing one not) is the card's default and the safe direction for cloud's "clear sample data": every id listed is one the loader is certain of, and a reader can tell unlisted rows exist by comparing inserted with the listed inserted.totals, as the body says. That comparison needs the counts, which the runtime fallback's thrown receipt does not carry (judgment 3).
  • finished is a required member of SeedLoadPartialWrites; the only constructor is notePartialWrites, so no other producer has to be taught it. A consumer that narrows the record (the ownership claim) is out of scope and untouched.
  • The runtime pin drives a mocked engine; the real-engine pin for the same receipt key is the objectql one, and the two producers share the reader and the projection by import, so the fallback's door pin is a producer pin, not an engine pin. Acceptable.
  • Carried tier: the fallback's missing counts on a throw (pre-existing, named in the body's Acceptance notes, no card filed); the failing dataset's own landed rows have no ids on the receipt (cloud's reading, out of scope here).

④ Design judgment

The fix is at the right layer (the loader's own partial-writes record, which already held the finished results) and keeps the shape #30348 set: one projection, two lists, one cap, two producers importing the same two symbols. The failing-dataset decision is argued from the loader's own retry rule rather than asserted, and every claim the order makes is pinned at a real door with real reds. Must-fix: none. Should-fix, in order: (1) in the runtime fallback's catch, spread inserted: written?.inserted ?? 0 and updated: written?.updated ?? 0 beside landed now that written is read there (:2134), so the schema's required counts are met and the describe's "its rows are in the counts" is true of both producers; failing that, file the carried card and name it in the body; (2) add "possibly empty, when the throw came before any dataset finished" to the describe (:1498-1499), since the changeset and the objectql pin promise [] and the describe only implies it; (3) add a loader-unit or door pin for a throw in pass 2 (every dataset finished, no failedObject, landed lists all); (4) harmonize the older catch comment at protocol-publish-batch.ts :155-160 ("No reading means the throw came before the loader wrote anything") with the new one at :170-176 ("no load ran"), since the first now reads as the only condition and is the weaker of the two. None blocks landing; landing waits on the still-running required contexts (Build Core, Test Core 1-8, Type Check · workspace, Lint) going green.

Implemented-by: claude/project-thread-crfqzt
Reviewed-by: session_01WGufzfgvGexKn9vLHsy89K

VERDICT: PASS

…its counts; pin a pass-2 throw

Contract-review round 1: the runtime fallback's thrown receipt carries
inserted/updated and names the stopped object; the landed describe says it
is empty when no dataset finished; a pass-2 throw lists every dataset as
finished; the older catch comment agrees with the new one.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzht5n1PKh96TqTfrvV
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

维护者速读

  • 改了什么:种子加载中途抛错时,发布回执 seedApplied 现在也带 landed,列出抛错前已经跑完的数据集落下的行 id(同一个投影、同样分 inserted/matched 两张表、同样 200 上限);抛错的那个数据集不列出。运行时的兜底种子应用走同一读取与投影,失败回执也补上了 schema 要求的 inserted/updated 条数,并在 error 里点名停在哪个对象。
  • 为什么改:metadata-protocol: the publish receipt's seedApplied drops the landed row ids the seed loader computes, so cloud cannot tell which rows an AI build's seed landed on (objectstack-ai/cloud#3598 ruling A) #30348 之后,加载一旦中途失败,回执只有条数没有 id,cloud 的「清理示例数据」拿不到前面已落库数据集的行,这些示例行就清不掉。
  • 风险与回滚:只在失败回执上多给一个已有的可选字段;成功路径不变。landed 缺席的含义收窄为"没有运行加载",抛错前一个数据集都没跑完时给 []。回滚即还原本 PR。
  • 先看:packages/metadata-protocol/src/seed-loader.ts 的 SeedLoadPartialWrites.finished 与 notePartialWrites;packages/metadata-protocol/src/protocol-publish-batch.ts 的 catch;packages/runtime/src/domains/packages.ts 兜底 catch。
  • 两分钟验证:pnpm --filter @objectstack/objectql exec vitest run src/publish-seed-receipt-landed-ids.test.ts,预期 10 例全绿,其中"load that threw"一例的 landed 只含先跑完的对象。
  • 需要你做的:无,席位落地。

席位意见:fable 契约复审一轮 PASS,无必改;四条建议全部采纳并核对了增量。抛错的那个数据集不列入 landed,因为加载器重试时会重读已有行,它的 id 在抛错时只知道一部分,列出来反而会误导清理方;它的行仍计入条数,对象名在 error 里。

席位核对 — ACCEPT · domain:spec#3 · head 7f2442b0338fb295c8ffd047fececd535ea75938

  • Shape: 11 files (+314 −25): SeedLoadPartialWrites.finished snapshots the datasets that finished (notePartialWrites); applySeedBodies' catch and the runtime fallback catch in packages/runtime/src/domains/packages.ts both fold it through the one seedAppliedLanded projection (same inserted / matched lists, same 200 cap); the seedApplied.landed describe and docblock in packages/spec/src/api/protocol.zod.ts and the regenerated reference page; pins at the objectql door, the runtime fallback and the loader; changesets.
  • Premise measured: at feat(spec,metadata-protocol): the publish receipt names the rows a seed created and the rows it only matched #30427's merge (a7b8876f) a thrown load's receipt carried counts only, the gap feat(spec,metadata-protocol): the publish receipt names the rows a seed created and the rows it only matched #30427's contract review 6656296448 named at protocol-publish-batch.ts :165-169.
  • Gates: at the head, metadata-protocol 316 files (22425 passed), objectql 551 (10989), runtime 543 (8647), spec 815 (25622), each typecheck exit 0; 105/105 derived gates exit 0 and --ran reconciles with 0 NOT-MEASURED; check-changeset-no-major exit 0 on semantic, the none control exits 1 (dev report and body). Ablations red as reported, round 1's counts ablation included. Check-runs on this head: 57 success, 8 skipped, 0 red; the body edit re-runs the body guards.
  • Contract review: 6660144246 on 4c02bddf: PASS, no must-fix. The seat read the round-1 delta (7 files, +95 −20): should-fix 1, the runtime fallback catch now returns inserted / updated from the reading (zero with no reading) and names the stopping object in error, as metadata-protocol's catch does, pinned; should-fix 2, the describe and docblock say landed is empty when no dataset finished; should-fix 3, a pass-2 throw is pinned (every dataset finished, landed lists all); should-fix 4, the older catch comment now agrees with the new one.
  • Declarations: Clause-②: yes (widening: …), Metadata-protocol: semantic (an existing key's absence changes meaning on a thrown load; no authorable key moves), @objectstack/spec and @objectstack/metadata-protocol minor, @objectstack/runtime patch. ADR-0087 not needed.
  • Accepted deviations: none.
  • Carried: none. The seed-ownership claim's reading of landedIds versus insertedIds stays out of scope, as the order said.
  • Landing: not governed (check-governed-merges --pr 30519: 339 lines). The hand-off is pr_ready plus automerge_enable, through the merge queue.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 5, 2026 14:49
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 5, 2026
Merged via the queue into main with commit 38d416f Nov 5, 2026
70 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

1 participant